# Development Run these checks from the repository root. The builder loads configuration, hooks, package recipes, and assets from the checkout, so use a complete source tree. ## Set up a development environment ```bash python3 -m pip install uv==0.11.23 uv sync --locked --extra dev --extra docs . .venv/bin/activate ``` `uv.lock` pins the dependency resolution across supported Python versions. Use `uv sync --locked` so the environment matches the committed lockfile and does not rewrite it during setup. On Debian or Ubuntu, install the system tools needed for mock integration tests and translation validation with `sudo apt install gettext mtools dosfstools xorriso squashfs-tools`. ## Validation ```bash python cli.py --check python -m pytest -q git ls-files -z '*.py' | xargs -0 -r flake8 pylint --errors-only void_builder cli.py python tools/check_bandit_regressions.py python tools/check_pylint_regressions.py bandit -r configs/custom_files -ll python tools/check_release_licenses.py --inventory-only git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n yamllint -c .yamllint .forgejo/workflows yamllint -c .yamllint-configs configs python -m build --no-isolation --wheel --outdir /tmp/pepvoid-dist find configs/custom_files -type f -name '*.po' -print0 \ | xargs -0 -r -n1 msgfmt --check --check-format -o /dev/null find configs/custom_files -type f -name '*.pot' -print0 \ | xargs -0 -r -n1 msgfmt --check-format -o /dev/null sphinx-build -b html -W --keep-going docs /tmp/pepvoid-docs ``` The Codeberg/Forgejo Actions workflows run configuration checks, Python and workflow YAML lint, wheel builds, translation validation, pytest, and the documentation build on Python 3.9–3.14. Jobs use the matching Python container and install a checksum-verified Node.js 24.22.2 runtime for JavaScript Actions. To run them on Codeberg, enable the repository's Actions unit and provide a Forgejo Runner with the `docker` label; the jobs request that label and use Python container images. Codeberg's hosted Actions service is currently limited, so a self-hosted runner may be required ([Codeberg Actions setup](https://docs.codeberg.org/ci/actions/), [Forgejo runner labels](https://forgejo.codeberg.page/docs/latest/admin/actions/configuration/)). The autonomous mock artifact matrix runs on Python 3.12 and covers ISO, raw disk, virtual machine, container, and tarball outputs. Pytest checks project SPDX headers, license texts, and attributions for selected bundled assets; CI separately parses tracked shell scripts with Bash. These checks do not prove that every third-party asset has been identified. The 26 IceWM XPM images have a hash-pinned maintainer confirmation recorded in `docs/licensing/maintainer-confirmations.json`; maintainers must add unresolved records whenever another asset's source, license, or permission is unclear. Consult the [licensing and provenance guide](licensing-and-provenance.md) and re-audit the manifest when bundled assets change. The `Release Licensing Gate` workflow verifies hash-pinned asset and repository-history records, then rejects version tags while the unresolved manifest contains files or trees; run `python3 tools/check_release_licenses.py` locally to check file hashes, vendored tree contents (including symlink targets), and repository-history integrity, and to see any blockers. `--inventory-only` validates all recorded evidence and rejects unreviewed payload assets while allowing documented release blockers, so regular CI remains useful during rights review. The release tag workflow runs the strict gate. It currently reports 25 generated wallpaper assets and 25 original-photo blobs retained in Git history; do not publish a release until rights evidence is recorded or those assets and historical blobs are removed. Generated `icon-theme.cache` files are excluded from the Tela Circle tree digest. The CI workflow builds the Sphinx documentation on pull requests with warnings treated as errors; the Forgejo Pages workflow also builds it before deployment. Flake8 checks style and Pyflakes diagnostics across all tracked Python source files. Pylint blocks Python errors across the matrix and compares all finding counts with the reviewed baseline in `tools/pylint-baseline.json` on Python 3.12. This keeps the known cleanup work visible while preventing new findings. The baseline is tied to Pylint 3.3.4 and targets Python 3.12; review the full report before fixing or updating it. Python 3.14 uses Pylint 4.1.3 for interpreter compatibility; the baseline check remains on Python 3.12. The first reviewed baseline recorded 628 diagnostics. The current baseline has 18 findings: seven `R0903` small-interface/data-record warnings, three `R0902` warnings for lifecycle managers, and four methods each reported for `R0913` and `R0917` because their constructors or command API carry 6–7 explicit inputs. There is no context-manager warning in the current baseline. Each remaining path/message pair has a rationale in `tools/pylint-baseline.json`; the baseline checker rejects missing or stale rationales as well as diagnostic count increases. Earlier cleanups cleared lazy logging interpolation, unspecified text encoding warnings, protected-member accesses, and duplicate code across bootloader configuration, preset aliases, Xfce package policy, and CLI code. The `R0903` cases are intentional single-operation facades, an abstract lifecycle interface, the engine registry, and a verification record; adding dummy public methods would make those APIs less clear. The `R0902` cases hold state with one build/toolchain lifecycle. The parameter-count findings belong to explicit dependency/context constructors and a subprocess-style runner API; they remain candidates for a request object if their call-site complexity grows. Recent cleanup removed local-import notices, simplified profile metadata dispatch, removed unnecessary `else` branches, and moved state initialization into constructors. System-configuration assembly is split into focused phases for profiles, platforms, package rules, user overrides, and repository selection. Configuration records and architecture/libc package filtering now live in the focused `config_models.py` and `config_package_policy.py` modules. Architecture-specific repository selection is isolated in `config_repository.py`; configuration assembly and artifact naming inputs are grouped into typed request records. The command-line argument definitions and profile/artifact naming helpers now live in `cli_parser.py` and `cli_helpers.py`, leaving `cli.py` focused on dispatching requested operations. CLI operations are grouped in `cli_actions.py`, option normalization in `cli_build_options.py`, and build setup/reporting in `cli_workflow.py`; the parser registers distinct argument groups through focused helpers. Target-system configuration actions are grouped in `customizer_actions.py` and `customizer_desktop_actions.py`, while `customizer.py` assembles and applies them. Artifact manifests and root filesystem exports live in `iso_artifacts.py`, the build driver in `iso_builder.py`, and architecture engine registration in `iso_engine_base.py`. Package repository selection and XBPS install retries now live in `package_installer.py`, leaving `ChrootManager` as the stable filesystem and chroot facade. Disk-image creation now separates boot staging, image filesystems, partition layout, and output conversion/compression. Disk-image verification now separates format detection, signature checks, compression checks, and optional qemu-img inspection; ISO verification separates header checks, directory listing, and member validation; tarball verification separates directory, package database, ELF architecture, and shadow-mode checks. Boot asset staging now lives in `boot_asset_stager.py`, separate from the architecture engine. The orchestrator now passes its retry setting through to the toolchain. The tarball resolver separates local references, HTTPS downloads, and automatic cache lookup. Artifact checksums, manifest assembly, and signing failure handling have separate helpers. Package installation separates stage preparation, local repository indexing, and filtering of custom packages without matching local archives. PRoot setup now separates the native, cached, and pinned-download paths; the command runner separates streamed and captured subprocess execution. Three broad exception catches remain at intentional error boundaries: the CLI converts unexpected top-level failures into a concise user-facing error, the build pipeline wraps unexpected failures while guaranteeing cleanup, and the customizer reports failures from extensible action implementations before propagating them. Download, extraction, signing, and toolchain paths catch their expected error types explicitly. The other findings remain a cleanup backlog, not a declaration that each finding is resolved. The baseline has been lowered to the current scan so future increases fail CI. Bandit checks all severity levels against exact reviewed source contexts in `tools/bandit-baseline.json`. It compares the path, rule, severity, confidence, message, and flagged source line (ignoring line-number changes), so a new call fails CI even when it uses a rule already present in that file. The baseline also requires a written rationale for every rule represented in its findings. The distributed applications under `configs/custom_files` are also scanned for medium and high severity Bandit findings. Their low severity subprocess notices are reviewed separately because package management and desktop integration require invoking host tools. The Bandit baseline is pinned to version 1.9.4 and contains 94 low severity notices. They concern subprocess calls using argument lists, the corresponding `subprocess` imports, and executable lookups through `PATH`. Rule-specific rationales are recorded beside the findings. The builder needs subprocesses to invoke host tools; the reviewed source contexts remain candidates for further cleanup and the baseline should not be regenerated just to silence them. Run `bandit -r void_builder cli.py -l` to review the full report before changing the baseline. ## Starting the Codeberg Actions runner The repository runner must be online on the host while Codeberg Actions jobs are waiting. Install `forgejo-runner` on that host and register a repository runner in **Settings → Actions → Runners**. Keep the UUID from the registration details and save the runner token in a separate file; never commit or paste the token into chat. Create a private config directory and token file, then create `~/.config/pepvoid-builder/codeberg-runner.json` with this structure: ```json { "uuid": "RUNNER-UUID-FROM-CODEBERG", "token_file": "/home/your-user/.config/pepvoid-builder/runner-token" } ``` Restrict access to both files (`chmod 700 ~/.config/pepvoid-builder` and `chmod 600` on the JSON config and token file). Start the runner from the repository root and leave the terminal open: ```sh ./tools/start-codeberg-runner.sh ``` The script reads the UUID and token path locally, then starts the registered runner with the `docker` label used by the repository workflows. Override the config or runner executable paths with `CODEBERG_RUNNER_CONFIG` and `FORGEJO_RUNNER_BIN` if needed. The runner token itself is never passed as a command-line argument or printed by the script.